moddbfandomcom-20200213-history
GLFW:Tutorials:Blob
In this tutorial I'll document the game Blob Shepherd I wrote (in C++) and give some pointers on software design. The ideas discussed here are somewhat influenced by the book C++ in action by Bartosz Milewski, which is a very good book in my opinion. Although the game uses the GLFW and OpenGL libraries, the focus will not be on those libraries. I'll explain how they are used here, but the game only uses a small part of them. The source code is well-commented, and this tutorial will only discuss things that might not be obvious from the code. Of course, that assumes you know C++ enough to read source code. The Game Blob Shepherd is a very simple little game where the objective is to protect your blob of green underwater goo from evil air bubbles. The blob is attracted to your mouse cursor, and that is how you control it. When a part of the blob touches a red air bubble, it dies. When you touch a yellow bubble, a dead part of your blob gets revived. The longer you stay alive the more bubbles will appear. My current high score is 8.5, try to beat it: high scores. The way I designed this game is pretty much by playing around. I had set up a simple particle system with particles that were attracted to the mouse cursor. When using a texture of balls with the alpha value falling off to the edges the particles looked just like a living green blob. I tweaked the parameters of the particle motion a bit more until I liked the behaviour. Then I decided to try and make some kind of game out of it. Initially I had 'crates' being shot through the screen which you had to avoid, but this looked awful. After I replaced them with bubbles it got somewhat better. I tweaked the way the bubbles were generated and the way they moved until they looked 'bubbly' enough. Finally, I added 'good' (as opposed to 'bad') bubbles, and finished it off with some on-screen text and a 'game over' menu. The whole process took about 6 hours spread over two days, of which at least two hours were spent on playing the game. Setting up the project When starting a project I usually import a bunch of files from other projects and start by making those work together. In this case I took a window wrapper for creating GLFW windows, a 2d vector class to make working with 2d motion easier, a general error handling header, and a font system. I also take a makefile from another project and adapt it to this project, but if you use an IDE that manages projects for you this is not necessary. Then I write a simple little main.cpp that puts all these together and see if it works. Usually it doesn't, because I forgot something or introduced some conflict, but it can be fixed quickly. Also, it may be desirable to cut some unnecessary stuff out of the components or extend them a little. When this 'base system' is in place I can start on the actual program. But before we go there I'll explain some of these existing components. The window class Because setting up a window and getting events works the same in most programs, a window class is pretty much the first thing I end up reusing in a new project. Having this in a class has the advantage that you can initialize and de-initialize the window by simply creating or destroying an object. This object does the following stuff: * Create a window, the client code can specify stuff like window size, color depth, window title, and whether the window is full-screen. * Poll for input. In the system I show here the client code can register an event handler class that has a member function called every time something happens. * Flip the screen buffers. * Close the window. The destructor does this. The event system is implemented with an abstract base class for event handlers, which contains a virtual function that is called for every event. class Abstract_Event_Handler { public: virtual ~Abstract_Event_Handler(){} virtual void Event(int event, bool state) = 0; }; The Event function takes two arguments. The first indicates what kind of event occurred. This can be a GLFW key code, or one of the constants defined at the top of window.hpp. The second argument gives some more information about the event. For keys this indicates whether the key was pressed (true) or release (false). Classes with any virtual functions at all should always have a virtual destructor (see this for an explanation). Having a class like this is more convenient than just having client code register a callback function. Function pointers point to top-level functions, and when you want the events to do something to an object this top level function would have to somehow know of this object - which requires some kind of global data. Using objects as event handlers allows you to embed extra information in such an object (see the functor pattern). The GLWindow class uses some hackery in order to work with GLFW. GLFW is a plain-C library, so it does use top-level functions as event callbacks. Because normal member functions can not be passed as function pointers the callbacks have to be static. class GLWindow { // (...) static GLWindow* instance; static void Key_Callback(int key, int action); static void Mouse_Button_Callback(int button, int action); static void Mouse_Pos_Callback(int x, int y); }; The instance pointer is needed to make these static functions have access to the current GLWindow object. It is set by the constructor, and reset to NULL by the destructor. This has another effect - because there is only one instance pointer, there can be only one GLWindow alive at any time. This is good though, because GLFW only allows one window to be open anyway. The rest of the window system should be more or less self-explanatory. If anything is unclear about this (or anything else), put a question in the 'discussion' for this wikipage. The error system Another component that I grabbed from another project is the header error.hpp. This is a very small header, containing some macros to make debugging easier. It defines two macros, ASSERT and THROW. The first is used to check an invariant, when something should always be true but you want to make sure. It is only used in debug builds. See the page about assertions. The second macro is used to throw exceptions, it attaches some information about the file and line where the exception came from in debug mode. I also got vector2.hpp from another project, but that header just implements a 2d vector class with the basic vector operations defined for it. The main function Then the next step was to make the main() function (in main.cpp) do something meaningful. In C++ projects that use exceptions I always surround the main code with a try block, so that exceptions do not result in an "abort trap" or whatever message, but print out something about the exception and exit the program properly. int main() { try{ // (...) return 0; } catch(std::exception& error){ std::cout << "Error: " << error.what() << std::endl; return 1; } } This only catches exceptions that inherit from std::exception, as those are the only exceptions I use in the program. You could add a catch(...) block below this to catch other exceptions. Note that the exception is called by reference. If you do not do this an exception (which might be of any class inheriting from std::exception) will get converted to a plain std::exception when it is caught, and information is lost (see this). The code in the main function that is actually meaningful does the following: It sets up the objects needed by the game, and then enters a loop. This loop computes the time elapsed this frame, reads input, flips the screen buffers and starts over. Once the world system is in place it will also update the world and draw it (in the first version of main.cpp the input reader was a simple stub that only caught escape presses). All major objects are created as local variables in the main function. Often programs have a special 'Game' or 'Application' class which contains the main objects, and in more complex programs this might be necessary. In this case though, this works fine and saves us some complexity. The objects are neatly destroyed when the main function returns. The particle system We now have a blank screen, waiting for escape to be pressed. This is useful, at least as a base for a program. The next step is to add some specific code that actually makes this program do anything. As mentioned earlier, I started with a simple particle system. The final version of this can be found in world.hpp and world.cpp. A particle system is a system that keeps track of a number of small object (the particles). There are rules for the behaviour of these particles, and every frame of the animation they are updated (most particles move) and then drawn. The basic properties of a particle are it's location and it's speed. If the time step (in seconds) of the current frame is dtime, then the new location of a particle is it's old location plus it's speed (distance travelled per second) multiplied by that time step. In code: particle.location += dtime * particle.speed; That code actually works (given that particle is an object with a location and a speed member, which are of the type Vector2f) with the vector class used here. On the long term, these classes can make code a lot more straightforward. The particles move in a coordinate system that corresponds to the screen pixel coordinates (0,0 is the top left corner, 1 unit is 1 pixel). This is not always the best idea, but in this case it kept stuff simple. The world in this game contains two types of particles, blobs and bubbles, described by the Blob and Bubble structs. Both have different rules for when and how their speed is changed. One thing they have in common is that while their location is updated (with the speed) every frame, the speed is only updated every X frames. Every particle contains a counter that is counted down by 1 every frame. When the counter is 0 the speed is updated and the counter is reset to X. This has two effects. Firstly it saves some processing, but more importantly it gives a certain delay to the responses of particles, which gives them a more 'chaotic' behaviour. When a blob has its speed updated it gets accelerated in the direction of the mouse pointer. This does not mean that its old speed gets replaced, instead a vector in the direction of the mouse gets added to it. This gives the motion of particles a certain consistency. The size of this vector that gets added is the acceleration. For blobs this is dependant on the distance between the particle and the mouse. Thus the mouse has more effect when it is closer to a particle. When this distance is less than 100 it is treated as though it is 100, because otherwise the particles would accelerate too much when the mouse gets real close to them, which would make them uncontrollable (they would 'shoot' off). With only this rule of being attracted to the mouse pointers the blobs exhibit an interesting behaviour, but still not quite what I wanted. They fly at the mouse and then shoot past it, get attracted back and shoot past it again, etc. It kind of looks like planets revolving around the sun. To make them behave like a blob I added a friction. Every frame a blob's speed is reduced by a factor depending on the current dtime. This way they will not keep overshooting the mouse indefinitely, they quickly lose 'altitude' and stick to the mouse pointer. And after tweaking the contstants a bit the blobs worked satisfactory. I had not started on the bubbles yet at this point. To implement this blob particle system I had made a World class that encapsulated them. This world class had an Update method, taking a dtime and the current mouse position as arguments, and a Draw method. For drawing I wrote a simple system, implemented in display.hpp and display.cpp, which is described later. Note that the first thing I did when adding the world system was to make an empty World class with the methods mentioned above, and integrate it into my main loop. At this point it did not do anything yet, but it's concept was already part of the program and thus I could slowly add parts of it and test them right away. This is a form of top-down development (I won't claim it is top-down design, I hardly did any designing in this project). You first integrate a stubbed implementation of what you want into the big picture of your program, and then start implementing. Another way to do this would be to first write a particle system, and then integrate it into the program. The disadvantage of that is that you can not test stuff as you are writing it, and you might encounter nasty surprises when integrating it. A good rule is to make sure your code can compile (almost) all of the time, and do a compile every time you've changed a few things. Making huge sweeping changes to a project and then trying to compile it is usually quite a painful process (you get 2000 compile errors) and is more prone to introduce bugs. The next thing are the bubbles. In the current version of the game they appear in groups and move in a wobbly, bubble-like way. In the first version they just appeared at random and moved in straight lines. On top of that they all had the same size. For this implementation very little was needed. Bubbles have a speed and location like blobs, and every frame their location is updated with this speed. For the randomizing the standard C function rand() is used, it's not great, but it's good enough. The first thing I added was code to make bubbles 'disappear' when they go off the screen, otherwise I'd end up with thousands of bubbles after the game had been running for a while. Next I wanted bubbles to appear in groups and on the edges of the screen. The function World::Generate_Bubble was added for this. It chooses a location on the left, right or bottom edge of the screen and places 1-5 bubbles at that point. They all have the same speed (always pointing away from the side they came from). I also added code to give bubbles a random size. Now we had groups of bubbles with the exact same speed sailing across the screen. Because their speeds are the same they stay together all the time, which does not look very good. Besides, I never see bubbles travelling in straight lines. To fix this a random acceleration was added to bubbles. Just like blobs, they do not get accelerated every frame (only once every 10 frames). This random acceleration has a bias upwards (negative y direction) to make bubbles float up a little bit. This looked a lot better. See the code in World::Update for the implementation. To make bubbles and blobs actually interact another function was added - World::Find_Collisions. It goes over every blob, and for every blob it checks every bubble to see whether they touch. To determine the distance between two objects squared distances are used, because they are cheaper to calculate than real distances (see Pythagoras). A new property was added to blobs, the bool dead. When a blob touches a bubble, it dies and no longer responds to the mouse (and the bubble also bursts). Bubbles are stored in an std::vector (see the C++ standard library page), which is a dynamic array. When a bubble is created it is added to the back, and when it is removed (because it touches a blob or goes outside of the screen) it is removed by copying the last bubble in the array to it's position and making the array one unit smaller. The next addition was 'good' and 'bad' bubbles. A boolean was added to every bubble, which caused it to be drawn in a different color, and the bubble-generator now created a groups of 'good' bubbles every now and then. When a blob collides with a bubble this boolean is checked, it the bubble is 'good' a dead blob is revived (see World::Revive_Blob) instead of killed. This makes gameplay a little more interesting. The display system The files display.hpp and display.cpp define a few utility functions for drawing in 2d. Init_GL sets up the OpenGL state with an orthogonal projection matrix (makes stuff be drawn without perspective) and blending enabled (because my textures are transparent). The Color struct and Set_Color functions make color management easier. These files also define the Sprite class, which encapsulates an OpenGL texture object. Texture objects are textures that are stored in video card memory (if possible) and can be used to easily switch texture. The constructor loads a texture from a TGA image file. This code (using both GLFW and OpenGL) creates the texture object: GLFWimage image; if (glfwReadImage(file, &image, GLFW_ORIGIN_UL_BIT) != GL_TRUE) THROW(std::runtime_error, "Invalid sprite file"); glGenTextures(1, &_id); glBindTexture(GL_TEXTURE_2D, _id); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, image.Format, image.Width, image.Height, 0, image.Format, GL_UNSIGNED_BYTE, reinterpret_cast(image.Data)); glfwFreeImage(&image); A GLFW image object is loaded first, and an exception is thrown if that fails. The call to glGenTextures tells OpenGL to create a new texture object id. If the first argument is more than 1 and the second points to an array (of unsigned integers) you can generate more than 1 id at a time. Next the object with this id is bound (it becomes the current texture) with glBindTexture. The next two calls tell OpenGL to use a linear filter when enlarging or shrinking the texture, this looks nicer than the alternative, GL_NEAREST, which does not blend texels. After setting this we upload the image data with glTexImage2D. The image.Format is the type of the data, and it was set by GLFW. Useful values for this are GL_ALPHA (only alpha channel), GL_LUMINANCE (only grayscale), GL_RGB (red, green, blue) and GL_RGBA (red, green, blue and alpha). GL_UNSIGNED_BYTE indicates that the data is stored as unsigned 8-bit values. The last argument is a pointer to block of data with a size of width * height * channels containing the image data. Finally, we release the GLFW image with glfwFreeImage. This whole image step could be left out by using glfwGenTexture2d, which loads an image and immediately uploads it as texture, replacing the call to glTexImage2D, but I needed some more control here because I allow 'pure alpha' textures, and the current version of GLFW does not (see the code for more about this). The destructor of the Sprite class releases it's texture object, and the Draw function draws a textured quad using the texture. Finishing it up At this point there was a more or less playable game, but it was a little too minimal. For starters, the game should respond when the player's blob is completely dead, and give a game over message. On top of that I wanted some feedback about the current level, how much blob was currently alive, and the frames per second. Being the lazy hacker that I am I started by putting all this stuff into the World class. The class (which at this point also took care of the mouse pointer) got a font member variable and a variable to keep track of the frames per second, and in it's drawing function it would calculate how much blob was alive and display that, the fps and the current level. Now to anybody familiar with the 'one responsibility per class' idea it should be obvious that the World class was getting too messy. Code tends to be cleaner and more understandable if a class has a single responsibility. I have a habit to push the limits of this rule a bit, but when I set out to implement the 'game over' system I realized that I had to separate some stuff out of the World class. I created an Interface class, which took over a few responsibilities: * The mouse pointer * Reading the input (so far a simple stub had been listening for escape keypresses) * Calculating the frames per second * Drawing game status text * The 'game over' menu The end result of this separation is no thing of beauty, I feel that the interface between this class and the rest of the program it a little clunky, but for this small project it is enough, and putting more effort into stuff that works satisfactory and is not in the way is usually not worth it (link). The way I approached this, just hack away and when stuff gets messy you 'refactor' it into a better shape, is often a rather practical way to do things. Many people favour big designs upfront, and for big professional systems they have a point, but when you are doing something small and you don't really know what it will look like yet, you can just let it grow this way. The Interface class is quite simple. It keeps some booleans to know it's current state, and when the game is in 'game over' state (_menu variable) it listens for 'y' or 'n' keypresses. It gets game information (level and blob vitality) passed in from the World object, and shows it in the top left corner. These little touches make the game give a somewhat less crude impression though. The end That's all there is to it. Of course the source code contains some stuff I didn't even mention here, but nothing magical. I mostly tried to show an approach to coding here, no brilliant algorithms or anything were demonstrated. One subsystem I did not explain is the Font class. This is closely tied up with the tool I wrote to generate font files, and I'm planning on writing another tutorial on that soon. Source code * Download the entire source code here Category:Tutorial Category:Postmortems